Skip to content

Add the SuperTokens lazy migration flow#74

Merged
mhamann merged 2 commits into
rownd:mainfrom
bcbogdan:feat/supertokens
Apr 23, 2026
Merged

Add the SuperTokens lazy migration flow#74
mhamann merged 2 commits into
rownd:mainfrom
bcbogdan:feat/supertokens

Conversation

@bcbogdan
Copy link
Copy Markdown
Contributor

@bcbogdan bcbogdan commented Apr 21, 2026

Summary

Adds SuperTokens lazy user migration support to the Android SDK.
When the SuperTokens config is passed aand a Rownd sign-in completes for a new_user, the SDK now calls the ${apiBasePath}/plugin/rownd/migrate endpoint.

Sync flow:

  • The app opts into migration by setting Rownd.config.supertokens with the SuperTokens app info.
  • Rownd registers an event listener for SignInCompleted.
  • When a sign-in completes for a new_user, the SDK reads the current Rownd access token from state.
  • The SDK calls the SuperTokens migration endpoint at

Summary by Sourcery

Add support for lazily migrating newly created Rownd users to SuperTokens when configured.

New Features:

  • Introduce SuperTokens configuration options on Android, including app info and API paths, via RowndConfig.
  • Trigger a SuperTokens migration call after a successful sign-in for newly created users when SuperTokens is configured.

Enhancements:

  • Add a utility function to invoke the SuperTokens migration endpoint using the current Rownd access token.

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented Apr 21, 2026

Reviewer's Guide

Implements a lazy SuperTokens user migration flow in the Android SDK by introducing SuperTokens configuration into RowndConfig, wiring a sign-in completion event listener in RowndClient, and adding a utility that POSTs the current Rownd access token to the SuperTokens migration endpoint for new users.

Sequence diagram for SuperTokens lazy migration on sign-in completion

sequenceDiagram
    actor User
    participant App
    participant RowndClient
    participant EventEmitter
    participant Store
    participant SuperTokensSync
    participant SuperTokensAPI

    User->>App: Trigger sign in
    App->>RowndClient: startSignIn()
    RowndClient->>EventEmitter: emit RowndEvent(SignInCompleted, data)

    EventEmitter->>RowndClient: invoke listener(event)
    alt event.event == SignInCompleted and user_type == new_user
        RowndClient->>Store: get currentState.auth.accessToken
        Store-->>RowndClient: accessToken
        RowndClient->>RowndClient: read config.supertokens.appInfo
        RowndClient->>SuperTokensSync: launch coroutine syncUserToSuperTokens(accessToken, appInfo)
        activate SuperTokensSync
        SuperTokensSync->>SuperTokensAPI: POST /auth/plugin/rownd/migrate
        SuperTokensAPI-->>SuperTokensSync: 2xx or error status
        SuperTokensSync-->>RowndClient: return
        deactivate SuperTokensSync
    else not new_user or missing config/token
        EventEmitter-->>RowndClient: listener returns without sync
    end
Loading

Class diagram for SuperTokens configuration and sync utility

classDiagram
    class RowndConfig {
        String appKey
        String? apiUrl
        Boolean debug
        Boolean enableSdkTelemetry
        Boolean enableSmartLinkPasteBehavior
        SuperTokensConfig? supertokens
    }

    class SuperTokensConfig {
        SuperTokensAppInfo appInfo
    }

    class SuperTokensAppInfo {
        String appName
        String apiDomain
        String apiBasePath = "/auth"
    }

    class RowndClient {
        RowndConfig config
        Store store
        EventEmitter eventEmitter
        Telemetry telemetry
        initRownd()
    }

    class SuperTokensSync {
        <<utility>>
        +suspend syncUserToSuperTokens(accessToken: String, appInfo: SuperTokensAppInfo)
    }

    RowndConfig --> SuperTokensConfig : has optional
    SuperTokensConfig --> SuperTokensAppInfo : has
    RowndClient --> RowndConfig : uses
    RowndClient ..> SuperTokensSync : calls
    SuperTokensSync --> SuperTokensAppInfo : uses
Loading

File-Level Changes

Change Details Files
Wire SuperTokens lazy migration trigger into the Rownd client lifecycle after sign-in completion for new users.
  • Register a global event listener on the Rownd eventEmitter in RowndClient.init
  • Filter events to only handle SignInCompleted events where user_type == "new_user"
  • Guard against uninitialized store and missing access token or SuperTokens config before proceeding
  • Launch a background coroutine on Dispatchers.IO to perform the SuperTokens sync call
android/src/main/java/io/rownd/android/Rownd.kt
Add SuperTokens configuration structures to the SDK configuration model so apps can opt into migration.
  • Introduce SuperTokensAppInfo with appName, apiDomain, and apiBasePath (default "/auth")
  • Introduce SuperTokensConfig wrapper containing SuperTokensAppInfo
  • Extend RowndConfig with a transient supertokens field for runtime configuration
android/src/main/java/io/rownd/android/models/RowndConfig.kt
Implement SuperTokens migration HTTP client helper used by the lazy migration flow.
  • Add syncUserToSuperTokens suspend function that constructs the SuperTokens migration URL from appInfo and calls /plugin/rownd/migrate
  • Send POST request with Authorization: Bearer header
  • Log an error when the response status is non-2xx and on exceptions, treating failures as non-fatal
  • Run the network call on Dispatchers.IO and ensure the HttpURLConnection is disconnected
android/src/main/java/io/rownd/android/util/SuperTokensSync.kt

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@qodo-code-review
Copy link
Copy Markdown

Review Summary by Qodo

Add SuperTokens lazy migration flow to Android SDK

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Adds SuperTokens lazy user migration flow to Android SDK
• Implements event listener for sign-in completion events
• Creates SuperTokens configuration models for app integration
• Calls migration endpoint when new user signs in
Diagram
flowchart LR
  A["Sign-in Completed"] -->|"new_user event"| B["Event Listener"]
  B -->|"Extract access token"| C["Rownd State"]
  C -->|"POST to migrate endpoint"| D["SuperTokens API"]
  D -->|"User migrated"| E["Sync Complete"]
Loading

Grey Divider

File Changes

1. android/src/main/java/io/rownd/android/Rownd.kt ✨ Enhancement +19/-0

Add SuperTokens migration event listener

• Added imports for RowndEventType and syncUserToSuperTokens utility
• Implemented event listener in RowndClient initialization
• Listener triggers on SignInCompleted event for new users
• Extracts access token and SuperTokens config, then calls migration function

android/src/main/java/io/rownd/android/Rownd.kt


2. android/src/main/java/io/rownd/android/models/RowndConfig.kt ✨ Enhancement +15/-0

Add SuperTokens configuration models

• Created SuperTokensAppInfo data class with app name, API domain, and base path
• Created SuperTokensConfig data class wrapping app info
• Added optional supertokens property to RowndConfig
• All new classes marked as @Serializable for JSON support

android/src/main/java/io/rownd/android/models/RowndConfig.kt


3. android/src/main/java/io/rownd/android/util/SuperTokensSync.kt ✨ Enhancement +28/-0

Implement SuperTokens migration sync utility

• New utility file implementing syncUserToSuperTokens suspend function
• Constructs migration endpoint URL from SuperTokens app info
• Makes POST request with Bearer token authorization
• Includes error handling with logging for failed migrations

android/src/main/java/io/rownd/android/util/SuperTokensSync.kt


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented Apr 21, 2026

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Migration request can hang🐞 Bug ☼ Reliability
Description
syncUserToSuperTokens performs a POST via HttpURLConnection without connect/read timeouts, so it may
block indefinitely and tie up Dispatchers.IO threads on sign-in. This bypasses the SDK’s existing
timeout configuration (defaultRequestTimeout) used by its Ktor clients.
Code

android/src/main/java/io/rownd/android/util/SuperTokensSync.kt[R14-24]

+    val base = "${appInfo.apiDomain}${appInfo.apiBasePath}"
+
+    try {
+        val conn = URL("$base/plugin/rownd/migrate").openConnection() as HttpURLConnection
+        conn.requestMethod = "POST"
+        conn.setRequestProperty("Authorization", "Bearer $accessToken")
+        val code = conn.responseCode
+        if (code !in 200..299) {
+            Log.e("Rownd.ST", "[Rownd->ST] migrate failed with status: $code")
+        }
+        conn.disconnect()
Evidence
The new migration request creates and uses HttpURLConnection but never sets
connectTimeout/readTimeout before calling responseCode, which can block indefinitely. The SDK
already defines RowndConfig.defaultRequestTimeout and applies it via Ktor’s HttpTimeout plugin,
demonstrating an established timeout policy that this new call bypasses.

android/src/main/java/io/rownd/android/util/SuperTokensSync.kt[14-24]
android/src/main/java/io/rownd/android/util/KtorApiClient.kt[55-58]
android/src/main/java/io/rownd/android/models/RowndConfig.kt[30-40]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`syncUserToSuperTokens` uses `HttpURLConnection` with no `connectTimeout`/`readTimeout`, so `responseCode` can block indefinitely.
### Issue Context
The SDK already has a standard timeout (`RowndConfig.defaultRequestTimeout`) applied in `KtorApiClient` via `HttpTimeout`. The migration call should honor the same timeout policy.
### Fix Focus Areas
- android/src/main/java/io/rownd/android/util/SuperTokensSync.kt[14-24]
- android/src/main/java/io/rownd/android/util/KtorApiClient.kt[55-58]
### Implementation notes
- Set `conn.connectTimeout` and `conn.readTimeout` (e.g., from `Rownd.config.defaultRequestTimeout` or passed-in timeout).
- Ensure `disconnect()` happens in a `finally` block after the request completes.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Bearer token sent unsafely 🐞 Bug ⛨ Security
Description
The migration flow sends the Rownd access token as a Bearer Authorization header to a URL built from
SuperTokens config without enforcing HTTPS or handling redirects safely. A misconfigured apiDomain
(http://) or redirect could expose the access token to an unintended endpoint.
Code

android/src/main/java/io/rownd/android/util/SuperTokensSync.kt[R14-20]

+    val base = "${appInfo.apiDomain}${appInfo.apiBasePath}"
+
+    try {
+        val conn = URL("$base/plugin/rownd/migrate").openConnection() as HttpURLConnection
+        conn.requestMethod = "POST"
+        conn.setRequestProperty("Authorization", "Bearer $accessToken")
+        val code = conn.responseCode
Evidence
RowndClient retrieves the current access token and passes it to syncUserToSuperTokens, which
constructs the destination URL from appInfo.apiDomain/apiBasePath and attaches it as a Bearer
token header. There is no check that the URL is HTTPS and no explicit redirect policy, so
configuration mistakes or redirects can leak the token.

android/src/main/java/io/rownd/android/Rownd.kt[103-108]
android/src/main/java/io/rownd/android/util/SuperTokensSync.kt[14-20]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The SDK sends a Rownd access token in an `Authorization: Bearer` header to a URL composed from config without validating HTTPS or guarding redirects.
### Issue Context
This is a sensitive credential; sending it over `http://` or allowing redirects can expose it.
### Fix Focus Areas
- android/src/main/java/io/rownd/android/util/SuperTokensSync.kt[14-20]
- android/src/main/java/io/rownd/android/Rownd.kt[103-108]
### Implementation notes
- Parse the URL and enforce `https` scheme (optionally allow `http` only when `Rownd.config.enableDebugMode == true`).
- Set `conn.instanceFollowRedirects = false` (or explicitly validate redirect targets before re-sending the request and never forward the Authorization header cross-host).
- Consider normalizing/validating `apiDomain` + `apiBasePath` to avoid surprising target URLs.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Migration errors lack details 🐞 Bug ◔ Observability
Description
When migration fails, the code logs only a status code or exception message without the
throwable/stack trace or response body. This makes diagnosing production migration failures
significantly harder.
Code

android/src/main/java/io/rownd/android/util/SuperTokensSync.kt[R21-27]

+        if (code !in 200..299) {
+            Log.e("Rownd.ST", "[Rownd->ST] migrate failed with status: $code")
+        }
+        conn.disconnect()
+    } catch (e: Exception) {
+        Log.e("Rownd.ST", "[Rownd->ST] migrate failed (non-fatal): ${e.message}")
+    }
Evidence
The new code logs errors without passing the Throwable to Log.e and doesn’t read
errorStream/response body. Elsewhere in the SDK, exceptions are logged with the throwable,
preserving stack traces for debugging.

android/src/main/java/io/rownd/android/util/SuperTokensSync.kt[21-27]
android/src/main/java/io/rownd/android/models/repos/StateRepo.kt[121-124]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Migration failures are logged without stack traces and without server error details.
### Issue Context
Without the throwable and error body, it’s difficult to debug why migrations fail in production.
### Fix Focus Areas
- android/src/main/java/io/rownd/android/util/SuperTokensSync.kt[21-27]
### Implementation notes
- Use `Log.e(tag, msg, e)` in the catch.
- For non-2xx responses, read `conn.errorStream` (if present) and include it in logs (redact any sensitive data if needed).
- Ensure streams are closed via `.use {}` and `disconnect()` in `finally`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • The RowndClient event listener creates a new CoroutineScope(Dispatchers.IO) for each sign-in; consider using an existing structured coroutine scope (e.g., tied to rowndContext or a lifecycle) to avoid unmanaged coroutines and potential leaks.
  • In syncUserToSuperTokens, HttpURLConnection is used without configuring connection/read timeouts or explicitly closing streams, which can lead to hanging requests or resource leaks; set reasonable timeouts and ensure any streams are closed.
  • The construction of the SuperTokens base URL ("${appInfo.apiDomain}${appInfo.apiBasePath}") assumes the domain and path are already well-formed; consider normalizing or validating these values (e.g., ensuring scheme and leading/trailing slashes) to avoid subtle URL issues across different configurations.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `RowndClient` event listener creates a new `CoroutineScope(Dispatchers.IO)` for each sign-in; consider using an existing structured coroutine scope (e.g., tied to `rowndContext` or a lifecycle) to avoid unmanaged coroutines and potential leaks.
- In `syncUserToSuperTokens`, `HttpURLConnection` is used without configuring connection/read timeouts or explicitly closing streams, which can lead to hanging requests or resource leaks; set reasonable timeouts and ensure any streams are closed.
- The construction of the SuperTokens base URL (`"${appInfo.apiDomain}${appInfo.apiBasePath}"`) assumes the domain and path are already well-formed; consider normalizing or validating these values (e.g., ensuring scheme and leading/trailing slashes) to avoid subtle URL issues across different configurations.

## Individual Comments

### Comment 1
<location path="android/src/main/java/io/rownd/android/util/SuperTokensSync.kt" line_range="17" />
<code_context>
+    val base = "${appInfo.apiDomain}${appInfo.apiBasePath}"
+
+    try {
+        val conn = URL("$base/plugin/rownd/migrate").openConnection() as HttpURLConnection
+        conn.requestMethod = "POST"
+        conn.setRequestProperty("Authorization", "Bearer $accessToken")
</code_context>
<issue_to_address>
**suggestion (performance):** Consider configuring connect and read timeouts on the HttpURLConnection.

Relying on default timeouts can cause this call to hang for a long time under poor network conditions. Please set explicit `connectTimeout` and `readTimeout` values so this background sync fails or recovers in a predictable timeframe.

Suggested implementation:

```
    try {
        val conn = URL("$base/plugin/rownd/migrate").openConnection() as HttpURLConnection
        conn.connectTimeout = 5_000  // 5 seconds
        conn.readTimeout = 10_000    // 10 seconds
        conn.requestMethod = "POST"
        conn.setRequestProperty("Authorization", "Bearer $accessToken")

```

If there is an existing configuration or constants file for network settings in this project, you may want to:
1. Extract the `5_000` and `10_000` values into named constants (e.g., `DEFAULT_CONNECT_TIMEOUT_MS`, `DEFAULT_READ_TIMEOUT_MS`) in that shared location.
2. Replace the inline literals here with those constants to keep timeout behavior consistent across the codebase.
</issue_to_address>

### Comment 2
<location path="android/src/main/java/io/rownd/android/util/SuperTokensSync.kt" line_range="16-27" />
<code_context>
+        val conn = URL("$base/plugin/rownd/migrate").openConnection() as HttpURLConnection
+        conn.requestMethod = "POST"
+        conn.setRequestProperty("Authorization", "Bearer $accessToken")
+        val code = conn.responseCode
+        if (code !in 200..299) {
+            Log.e("Rownd.ST", "[Rownd->ST] migrate failed with status: $code")
</code_context>
<issue_to_address>
**suggestion (performance):** Consider consuming and closing the response body to ensure proper connection reuse.

Relying only on `responseCode` may leave the body unread, which can interfere with HTTP connection pooling. Read and close the `inputStream` or `errorStream` (even if you discard the data) before disconnecting to maximize connection reuse.

```suggestion
    try {
        val conn = URL("$base/plugin/rownd/migrate").openConnection() as HttpURLConnection
        conn.requestMethod = "POST"
        conn.setRequestProperty("Authorization", "Bearer $accessToken")

        val code = conn.responseCode

        // Consume and close the response body (success or error) to enable connection reuse
        val responseStream = try {
            if (code in 200..299) conn.inputStream else conn.errorStream
        } catch (_: Exception) {
            null
        }

        responseStream?.use { stream ->
            val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
            while (stream.read(buffer) != -1) {
                // Discard response body
            }
        }

        if (code !in 200..299) {
            Log.e("Rownd.ST", "[Rownd->ST] migrate failed with status: $code")
        }

        conn.disconnect()
    } catch (e: Exception) {
        Log.e("Rownd.ST", "[Rownd->ST] migrate failed (non-fatal): ${e.message}")
    }
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread android/src/main/java/io/rownd/android/util/SuperTokensSync.kt Outdated
Comment thread android/src/main/java/io/rownd/android/util/SuperTokensSync.kt Outdated
Comment thread android/src/main/java/io/rownd/android/util/SuperTokensSync.kt Outdated
Comment thread android/src/main/java/io/rownd/android/util/SuperTokensSync.kt Outdated
Comment thread android/src/main/java/io/rownd/android/util/SuperTokensSync.kt Outdated
Comment thread android/src/main/java/io/rownd/android/util/SuperTokensSync.kt Outdated
Comment thread android/src/main/java/io/rownd/android/util/SuperTokensSync.kt Outdated
@mhamann mhamann merged commit ff6ec71 into rownd:main Apr 23, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants